package edu.nps.ai.agent;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;

import edu.nps.ai.astar.AStar;
import edu.nps.ai.weapon.Weapon;

/**
 * Abstract implementation of the agent
 *
 * @author Timothy Riley
 */
public abstract class AbstractAgent implements Agent {
	/**
	 * Do not log to file
	 */
	private static final String DO_NOT_LOG_TO_FILE = "none";
	/**
	 * Holds the AStar search utility
	 */
	private AStar astar;
	/**
	 * Holds the list of weapons
	 */
	private Map<String, Weapon> weaponMap = new HashMap<String, Weapon>();
	/**
	 * Holds the delay to slow and speed up the application
	 */
	private Integer delay;

	/**
	 * Holds the Logger
	 */
	private Logger logger;

	/**
	 * Holds the print stream
	 */
	private PrintStream printStream;

	/**
	 * Holds the buffered reader
	 */
	private BufferedReader bufferedReader;
	/**
	 * @return the astar
	 */
	public AStar getAstar() {
		return astar;
	}
	/**
	 * @return the weaponMap
	 */
	public Map<String, Weapon> getWeaponMap() {
		return weaponMap;
	}
	/**
	 * Initialize the environment
	 *
	 * @param host
	 *            The host name of the server
	 * @param port
	 *            The port the server is running one
	 * @param userName
	 *            The user name to create in the environment
	 * @param password
	 *            The password for the user created
	 * @param delay
	 *            The delay between commands
	 * @param logFile
	 *            The log file
	 */
	public void initialize(final String host, final Integer port, final String userName, final String password, final Integer delay, final String logFile) {
		if (host == null || port == null || userName == null || password == null || delay == null || logFile == null) {
			throw new IllegalArgumentException("All arguments are required");
		}
		this.astar = new AStar();
		weapons();
		this.delay = delay;
		//Add Shutdown Hook
		AgentShutdown agentShutdown = new AgentShutdown(this);
        Thread hook = new Thread(agentShutdown);
        Runtime.getRuntime().addShutdownHook(hook);

        //Initialize Logger
		if (!DO_NOT_LOG_TO_FILE.equals(logFile)) {
			System.setProperty("logFile", logFile);
			logger = LoggerFactory.getLogger("all");
		}
		log("hostname: " + host);
		log("port: " + port);
		log("username: " + userName);
		log("password: " + password);
		log("delay: " + delay);
		log("logfile: " + logFile);

		//Connect and BootStrap The game
		try {
			connect(host, port);
			bootstrap(userName, password);
		} catch (Exception e) {
			log("Connecting to the server failed", e);
			throw new RuntimeException("Connecting to the server failed", e);
		}

	}

	/**
	 * Initialize the weapons
	 */
	private void weapons() {
		InputStream inputStream = null;
		try {
			ClassPathResource classPathResource = new ClassPathResource("/weapon.csv");
			inputStream = classPathResource.getInputStream();
			List<String> weaponData = IOUtils.readLines(inputStream);
			for (String line : weaponData) {
				String[] data = line.split(",");
				if (data.length != 5) {
					throw new RuntimeException("The weapon data is invalid");
				}
				Integer ordinal = Integer.parseInt(data[0]);
				String location = data[1];
				String name = data[2];
				String affectsAll = data[3];
				String singleUse = data[4];
				Weapon weapon = new Weapon(ordinal, name, location, affectsAll, singleUse);
				weaponMap.put(name, weapon);
			}
		} catch (Exception e) {
			throw new RuntimeException("Initialization failed", e);
		} finally {
			if (inputStream != null) {
				IOUtils.closeQuietly(inputStream);
			}
		}
	}

	/**
	 * Initialize the connection to the server with the host and port provided
	 * @param host The host
	 * @param port The port
	 * @throws Exception Thrown
	 */
	private void connect(final String host, final Integer port) throws Exception {
		log("Connecting to server host:[" + host + "] port:[" + port + "]");
		Socket socket = new Socket(host, port);
		InputStream inputStream = socket.getInputStream();
		OutputStream outputStream = socket.getOutputStream();
		InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
		printStream = new PrintStream(outputStream, true);
		bufferedReader = new BufferedReader(inputStreamReader);
		log("Connected to server");
	}

	/**
	 * Initialize the server environment (e.g. ensure user exists)
	 * @param userName The userName to connect with
	 * @param password The password of the user
	 */
	private void bootstrap(final String userName, final String password) {
		send("connect " + userName + " " + password);
		sleep(1000, "Connecting");
		List<String> output = listen();
		Boolean playerDoesNotExist = verify(output, "player does not exist");
		if (playerDoesNotExist) {
			send("create " + userName + " " + password);
			output = listen();
			Boolean created = verify(output, "*** Created ***");
			if (!created) {
				log("The user could not be created");
				throw new RuntimeException("The user could not be created");
			}
		}
	}

	/**
	 * Destroy the environment
	 */
	public void destroy() {
		if (printStream != null && bufferedReader != null) {
			send("@quit");
			try {
				printStream.flush();
				printStream.close();
				bufferedReader.close();
			} catch(Exception e) {
				log("Failed to close connection", e);
				throw new RuntimeException("Failed to close the connection", e);
			}
		}
		System.out.flush();
	}

	/**
	 * Helper method to execute the command
	 * @param command The command to execute
	 */
	protected void send(final String command) {
		if (command == null) {
			throw new IllegalArgumentException("The command is requried");
		}
		if (printStream == null || bufferedReader == null) {
			throw new IllegalStateException("Initialization must be called prior to executing a command");
		}
		log("Execute: [" + command + "]");
		printStream.println(command);
		sleep();
	}

	/**
	 * Helper method to listen for any output from the socket
	 * @return The list of output statements
	 */
	protected List<String> listen() {
		if (bufferedReader == null) {
			throw new IllegalStateException("Initialization must be called prior to listening");
		}
		List<String> output = new ArrayList<String>();
		try {
		    while(bufferedReader.ready()) {
		    	String line = bufferedReader.readLine();
		    	output.add(line);
		    	log("Output: [" + line + "]");
		    }
		} catch (Exception e) {
			log("Listening Failed", e);
			throw new RuntimeException("Listening Failed", e);
		}
		return output;
	}

	/**
	 * Sleep for the delay period provided
	 */
	protected void sleep() {
		sleep(delay, "Delay");
	}

	/**
	 * Sleep for the delay period provided
	 */
	protected void sleep(Integer millis, String reason) {
		if (millis == null) {
			throw new IllegalArgumentException("The sleep delay is requried");
		}
		try {
			log("Sleeping-" + reason);
		    Thread.sleep(millis);
		} catch (InterruptedException e) {
			log("Sleeping Failed", e);
			throw new RuntimeException("Sleeping Failed", e);
		}
	}

	/**
	 * Verify if the check string exists in the output
	 * @param output The list of output results
	 * @param check The string to check
	 * @return True if the string exists
	 */
	protected Boolean verify(final List<String> output, final String check) {
		if (output == null || check == null) {
			throw new IllegalArgumentException("The output is required");
		}
		Boolean exists = false;
		for (String line : output) {
			if (line.contains(check)) {
				exists = true;
				break;
			}
		}
		return exists;
	}


	/**
	 * Verify if the check string exists in the output
	 * @param output The list of output results
	 * @param check The string to check
	 * @return True if the string exists
	 */
	protected Boolean verify(final List<String> output, final String[] checkArray) {
		if (output == null || checkArray == null) {
			throw new IllegalArgumentException("The output is required");
		}
		Boolean exists = false;
		for (String line : output) {
			for (String check : checkArray) {
				if (line.contains(check)) {
					exists = true;
					break;
				}
			}
		}
		return exists;
	}


	/**
	 * Helper method to get the current location
	 * @param output The output to search
	 * @return The current location
	 */
	protected String location() {
		send("look");
		List<String> output = listen();
		String location = null;
		for (String line : output) {
			if (line.contains("(")) {
				location = line.substring(0, line.indexOf(" ("));
				log("Current location [" + location + "]");
				break;
			}
		}
		return location;
	}

	/**
	 * @return the weaponMap
	 */
	public Weapon effectiveWeapon() {
		Weapon weapon = null;
		List<Weapon> weaponList = new ArrayList<Weapon> (weaponMap.values());
		Collections.sort(weaponList);
		Collections.reverse(weaponList);
		for (Weapon current : weaponList) {
			Integer effectiveness = current.getEffectiveness();
			if (effectiveness == null || effectiveness == 100) {
				weapon = current;
				break;
			}
		}
		return weapon;
	}

	/**
	 * Wrap the logging infrastructure
	 * @param message The message to log
	 */
	protected void log(final String message) {
		log(message, null);
	}

	/**
	 * Wrap the logging infrastructure
	 * @param message The message to log
	 * @param throwable The exception associated with the message
	 */
	protected void log(final String message, final Throwable throwable) {
		if (message == null) {
			throw new IllegalArgumentException("The message is required");
		}
		if (logger != null) {
			if (throwable == null) {
				logger.info(message);
			} else {
				logger.info(message, throwable);
			}
		} else {
			System.out.println(message);
		}
	}
}
